package net.bithappens.hnqe.impl.rest.handler;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import net.bithappens.hnqe.IllegalParamException;
import net.bithappens.hnqe.InvalidQueryException;
import net.bithappens.hnqe.graph.EntryValue;
import net.bithappens.hnqe.query.Atom;
import net.bithappens.hnqe.query.AtomAttribute;
import net.bithappens.hnqe.query.AtomConstant;
import net.bithappens.hnqe.query.ConstraintCompare;
import net.bithappens.hnqe.query.ConstraintCompare.ConstraintType;
import net.bithappens.hnqe.query.ConstraintMath;
import net.bithappens.hnqe.query.ConstraintMath.MathOperation;
import net.bithappens.hnqe.query.InvalidIdException;
import net.bithappens.hnqe.query.Link;
import net.bithappens.hnqe.query.Link.MatchType;
import net.bithappens.hnqe.query.QueryElement;
import net.bithappens.hnqe.query.QueryFactory;
import net.bithappens.hnqe.query.QueryTree;
import net.bithappens.hnqe.query.Relation;
import net.bithappens.hnqe.query.RelationMember;
import net.bithappens.hnqe.query.ValueNode;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.sun.jersey.core.util.Base64;

public class QueryXMLParser {

	private final static String namespace = "http://bithappens.net/hnqe/Query";
	
	private Logger log = Logger.getLogger(this.getClass());
	
	private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S");
	
	private NumberFormat nf = NumberFormat.getInstance(Locale.US);
	
	private Map<String, QueryElement> elements = new HashMap<String, QueryElement>();
	
	private StreamSource schemaXSD;

	private URL schemaURL;
	
	
	
	
	public QueryXMLParser() throws IOException {

		schemaURL = QueryXMLParser.class.getClassLoader().getResource("Query.xsd");

	}
	
	
	private boolean isValidQueryXML(String queryXML) {

		try{
			
			SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI );

			Schema schema = factory.newSchema(schemaURL);
			
			Validator validator = schema.newValidator();
								
			Source source = new StreamSource(new java.io.StringReader(queryXML));

			validator.validate(source);
			
			return true;
			
		} catch (Exception e) {
			log.error("The given queryXML is not valid: " + e.getMessage(),e);
			return false;
		}
	}
	
	

	public QueryTree parse(String queryXML) throws InvalidQueryException{
		
		if(isValidQueryXML(queryXML)){

			try{
				
				DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
				
				factory.setNamespaceAware(true);
				
				DocumentBuilder documentBuilder = factory.newDocumentBuilder();
				
				InputStream stream = new ByteArrayInputStream(queryXML.getBytes());
				
				Document doc = documentBuilder.parse(stream);
				
				doc.getDocumentElement().normalize();
				
				
				parseDeclaration(getChildsOfXMLElementNode(doc.getElementsByTagNameNS(namespace, "declaration")));
				
				return parsePattern(getChildsOfXMLElementNode(doc.getElementsByTagNameNS(namespace, "pattern")));
				
			} catch (Exception e) {
				throw new InvalidQueryException("The given queryXML couldn't be parsed: "+e.getMessage());
			}
			
		} else {
			throw new InvalidQueryException("The given queryXML is not valid or couldn't be validated");
		}
		
	}
	
	
	private String decodeBase64(String base64){
		return new String(Base64.decode(base64));
	}

	private EntryValue getEntryValue(Node valueNode) throws IllegalParamException, ParseException {
		
		String type = valueNode.getAttributes().getNamedItem("type").getNodeValue();
		
		String valueAsBase64 = valueNode.getTextContent();
				
		String value = decodeBase64(valueAsBase64.trim());
		
		if("Boolean".equals(type)){
			return new EntryValue(Boolean.valueOf(value));
		}
		
		if("Date".equals(type)){
			return new EntryValue(sdf.parse(value));
		}
		
		if("Double".equals(type)){
			return new EntryValue(Double.valueOf(value));
		}
		
		if("Float".equals(type)){
			return new EntryValue(Float.valueOf(value));
		}
		
		if("Integer".equals(type)){
			return new EntryValue(Integer.valueOf(value));
		}
		
		if("Number".equals(type)){
			return new EntryValue(nf.parse(value));
		}
		
		if("String".equals(type)){
			return new EntryValue(value);
		}
		return null;
	}
		
	private NodeList getChildsOfXMLElementNode(NodeList listWithSingleElement){
		
		for (int i = 0; i < listWithSingleElement.getLength(); i++) {

			Node declaration = listWithSingleElement.item(i);
		
			if(declaration.getNodeType() == Node.ELEMENT_NODE){
				
				return declaration.getChildNodes();
				
			}
			
		}
		
		return null;
	}

	
	
	private void parseDeclaration(NodeList declarationNodes) throws Exception {
		
		if(declarationNodes != null){
			
			for (int i = 0; i < declarationNodes.getLength(); i++) {
			
				Node element = declarationNodes.item(i);
				
				if(element.getNodeType() == Node.ELEMENT_NODE){
			
					if("relation".equals(element.getNodeName())){
						
						createTypeElement(element, true);
						
					}
				
					if("node".equals(element.getNodeName())){
						
						createTypeElement(element, false);
						
					}
					
					if("valueNode".equals(element.getNodeName())){
						
						String id = element.getAttributes().getNamedItem("id").getNodeValue();
						
						URI uri = null;
						if(element.getAttributes().getNamedItem("uri") != null){
							uri = new URI(element.getAttributes().getNamedItem("uri").getNodeValue());
						}
						
						EntryValue value = null;
						if(element.getChildNodes() != null){
							
							NodeList valueElement = element.getChildNodes();
							
							for (int j = 0; j < valueElement.getLength(); j++) {
							
								Node valueNode = valueElement.item(j);
							
								if(valueNode.getNodeType() == Node.ELEMENT_NODE){
									
									value = getEntryValue(valueNode);
									
								}
							}
						}
						elements.put(id, new ValueNode(id, uri, value));
					}
				}
			}
		}
	}

	private void createTypeElement(Node element, boolean isRelation) throws URISyntaxException, InvalidIdException {
		
		String id = element.getAttributes().getNamedItem("id").getNodeValue();
		
		URI uri = null;
		if(element.getAttributes().getNamedItem("uri") != null){
			uri = new URI(element.getAttributes().getNamedItem("uri").getNodeValue());
		}
		
		String type = null;
		if(element.getAttributes().getNamedItem("type") != null){
			type = element.getAttributes().getNamedItem("type").getNodeValue();
		}
		
		if(isRelation){
			elements.put(id, new Relation(id, uri, type));
		} else {
			elements.put(id, new net.bithappens.hnqe.query.Node(id, uri, type));
		}
		
	}


	private QueryTree parsePattern(NodeList patternNodes) throws Exception{
		
		if(patternNodes != null){
			
			NodeList restrictions = getChildsOfXMLElementNode(patternNodes);
			
			for (int i = 0; i < patternNodes.getLength(); i++) {
				
				Node element = patternNodes.item(i);
				
				if(element.getNodeType() == Node.ELEMENT_NODE){
					
					if("link".equals(element.getNodeName())){
						return processLink(element);
					}
										
					if("constraint".equals(element.getNodeName())){
						return processConstraint(element);
					}
					
					if("math".equals(element.getNodeName())){
						return processMath(element);
					}
					
					if("logic".equals(element.getNodeName())){
						return processLogic(element);
					}
					
				}
			}
		}
		return null;
	}

	
	private QueryTree processLink(Node linkElement) throws InvalidQueryException, IllegalParamException {
				
		NodeList linkEntries = linkElement.getChildNodes();
		
		Relation rel = null;
		
		List<RelationMember> incidents = new ArrayList<RelationMember>();
		
		for (int i = 0; i < linkEntries.getLength(); i++) {
			
			Node element = linkEntries.item(i);
			
			if(element.getNodeType() == Node.ELEMENT_NODE){
				
				if("relation".equals(element.getNodeName())){
					
					rel = getElementById(element.getAttributes().getNamedItem("elementId").getNodeValue(), Relation.class);
					
				}
				
				
				if("node".equals(element.getNodeName())){

					incidents.add(
							getElementById(element.getAttributes().getNamedItem("elementId").getNodeValue(), 
									net.bithappens.hnqe.query.Node.class));
					
				}
				
				if("valueNode".equals(element.getNodeName())){

					incidents.add(
							getElementById(element.getAttributes().getNamedItem("elementId").getNodeValue(), 
									ValueNode.class));
					
				}
			}
		}
		
		
		if("any".equals(linkElement.getAttributes().getNamedItem("match").getNodeValue())){
			return new Link(rel, MatchType.ANYCONNECTION, incidents.toArray(new RelationMember[incidents.size()]));
		} else {
			return new Link(rel, MatchType.EXACTMATCH, incidents.toArray(new RelationMember[incidents.size()]));
		}
	}	
	

	private QueryTree processConstraint(Node constraintElement) throws Exception {

		NodeList constraintEntries = constraintElement.getChildNodes();
		
		Atom constrainee = null;
		
		List<Atom> params = new ArrayList<Atom>();
		
		for (int i = 0; i < constraintEntries.getLength(); i++) {
			
			Node element = constraintEntries.item(i);
			
			if(element.getNodeType() == Node.ELEMENT_NODE){
				
				if("constrainee".equals(element.getNodeName())){
					constrainee = getAttributeOrValueAtom(getFirstChildNodeElement(element.getChildNodes()));
				}
								
				if("param".equals(element.getNodeName())){
					params.add(getAttributeOrValueAtom(getFirstChildNodeElement(element.getChildNodes())));
				}
			}
		}
		
		
		if("==".equals(constraintElement.getAttributes().getNamedItem("condition").getNodeValue())){
			return new ConstraintCompare(constrainee, ConstraintType.EQUAL, params.toArray(new Atom[params.size()]));
		} else if("!=".equals(constraintElement.getAttributes().getNamedItem("condition").getNodeValue())) {
			return new ConstraintCompare(constrainee, ConstraintType.NONEQUAL, params.toArray(new Atom[params.size()]));
		} else if(">".equals(constraintElement.getAttributes().getNamedItem("condition").getNodeValue())) {
			return new ConstraintCompare(constrainee, ConstraintType.GREATHER, params.toArray(new Atom[params.size()]));
		} else if(">=".equals(constraintElement.getAttributes().getNamedItem("condition").getNodeValue())) {
			return new ConstraintCompare(constrainee, ConstraintType.GREATHEREQUAL, params.toArray(new Atom[params.size()]));
		} else if("<".equals(constraintElement.getAttributes().getNamedItem("condition").getNodeValue())) {
			return new ConstraintCompare(constrainee, ConstraintType.LESS, params.toArray(new Atom[params.size()]));
		} else if("<=".equals(constraintElement.getAttributes().getNamedItem("condition").getNodeValue())) {
			return new ConstraintCompare(constrainee, ConstraintType.LESSEQUAL, params.toArray(new Atom[params.size()]));
		} else {
			return new ConstraintCompare(constrainee, ConstraintType.MATCHREGEX, params.toArray(new Atom[params.size()]));
		}
	}
	

	private QueryTree processMath(Node mathElement) throws Exception {
		
		NodeList mathEntries = mathElement.getChildNodes();
		
		String comparator = mathElement.getAttributes().getNamedItem("comparator").getNodeValue();
		
		Atom result = null; 
		
		for (int i = 0; i < mathEntries.getLength(); i++) {
			
			Node element = mathEntries.item(i);
			
			if(element.getNodeType() == Node.ELEMENT_NODE){
				
				if("result".equals(element.getNodeName())){
					result = getAttributeOrValueAtom(getFirstChildNodeElement(element.getChildNodes()));
					break;
				}
			}
		}
		
		ConstraintMath mathConstraint = null;
		
		if("=".equals(comparator)){
			mathConstraint = QueryFactory.MATH_EQUAL(result);
		} else if(">".equals(comparator)) {
			mathConstraint = QueryFactory.MATH_GEATHER(result);
		} else if(">=".equals(comparator)) {
			mathConstraint = QueryFactory.MATH_GREATHEREQUAL(result);
		} else if("<".equals(comparator)) {
			mathConstraint = QueryFactory.MATH_LESS(result);
		} else {
			mathConstraint = QueryFactory.MATH_LESSEQUAL(result);
		}
		
		for (int i = 0; i < mathEntries.getLength(); i++) {
			
			Node element = mathEntries.item(i);
			
			if(element.getNodeType() == Node.ELEMENT_NODE){
				
				if("operant".equals(element.getNodeName())){
					
					Atom param = getAttributeOrValueAtom(
							getFirstChildNodeElement(
								getFirstChildNodeElement(element.getChildNodes()).getChildNodes()));
					
					
					String operator = element.getAttributes().getNamedItem("operator").getNodeValue();
					
					if("+".equals(operator)){
						mathConstraint.addCalcStep(MathOperation.ADD, param);
					} else if("-".equals(operator)) {
						mathConstraint.addCalcStep(MathOperation.SUB, param);
					} else if("*".equals(operator)) {
						mathConstraint.addCalcStep(MathOperation.MUL, param);
					} else {
						mathConstraint.addCalcStep(MathOperation.DIV, param);
					} 
				}
			}
		}
				
		return mathConstraint;
	}

	
	private QueryTree processLogic(Node logicElement) throws Exception {
		
		List<QueryTree> params = new ArrayList<QueryTree>();
		
		NodeList operants = logicElement.getChildNodes();
		
		for (int i = 0; i < operants.getLength(); i++) {
			
			Node element = operants.item(i);
			
			if(element.getNodeType() == Node.ELEMENT_NODE){
				
				params.add(parsePattern(element.getChildNodes()));

			}
		}
		
		String operator = logicElement.getAttributes().getNamedItem("operation").getNodeValue();
		
		if("and".equals(operator)){
			return QueryFactory.AND(params.toArray(new QueryTree[params.size()]));
		} else if("or".equals(operator)) {
			return QueryFactory.OR(params.toArray(new QueryTree[params.size()]));
		} else {
			if(params.size() != 1){
				throw new InvalidQueryException("The logical operation must contain a single sub element");
			}
			return QueryFactory.NOT(params.get(0));
		} 
	}

	
	
	
	
	private <T> T getElementById(String id, Class<T> clazz) throws InvalidQueryException {
		
		QueryElement element = elements.get(id);
		
		if(element == null){
			throw new InvalidQueryException("The elementId \""+id+"\" was not declared bevore");
		}
		
		if(element.getClass() != clazz){
			throw new InvalidQueryException("The elementId \""+id+"\" was used for an invalid type " +
					"("+clazz.getName()+"!="+element.getClass().getName()+")");
		} 
		
		return (T) element;
	}
	
	private Node getFirstChildNodeElement(NodeList childNodes) {

		for (int i = 0; i < childNodes.getLength(); i++) {
			
			Node element = childNodes.item(i);
			
			if(element.getNodeType() == Node.ELEMENT_NODE){
				return element;
			}
		}
	
		return null;
	}

	
	private Atom getAttributeOrValueAtom(Node atomXMLNodeElement) throws InvalidQueryException, IllegalParamException, ParseException {

		if("attribute".endsWith(atomXMLNodeElement.getNodeName())){
			
			String id = atomXMLNodeElement.getAttributes().getNamedItem("elementId").getNodeValue();
			
			if(elements.containsKey(id)){
				QueryElement elem = elements.get(id);
				return new AtomAttribute(elem, atomXMLNodeElement.getAttributes().getNamedItem("name").getNodeValue());	
			} else {
				throw new InvalidQueryException("The elementId \""+id+"\" was used for an invalid type"); 
			}
			
		} else {
			return new AtomConstant(getEntryValue(atomXMLNodeElement));	
		}
	}
}

